﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace ProLinq.Mapping
{
	internal class MappingConfiguration<TSource, TResult> : MappingConfiguration
	{
		private Mapper _mapper;

		public MappingConfiguration(Mapper mapper)
		{
			this._mapper = mapper;
		}

		public Func<TSource, TResult> New { get; set; }
		public Func<TSource, TResult, TResult> Map { get; set; }

		public Dictionary<Object, Object> ObjectCache = new Dictionary<object, object>(); 

		public List<MemberMap> Maps { get; set; } 

		protected override void CompileInternal()
		{
			var sw1 = new Stopwatch();
			sw1.Start();

			var sourceType = typeof(TSource);
			var resultType = typeof(TResult);

			var proprtyMap = CreatePropertyMap(sourceType, resultType)
				.Where(map => map.ResultMember is FieldInfo || ((PropertyInfo)map.ResultMember).CanWrite).ToArray();


			var param = Expression.Parameter(sourceType, "s");

			var newExpression = Expression.New(resultType);

			var mapMethodTemplate = _mapper.GetType().GetMethods().First(m => m.Name == "Map" && m.GetParameters().Length == 1);


			var lambda = Expression.Lambda<Func<TSource, TResult>>(newExpression, param);
			

			var labelReturn = Expression.Label(resultType);
			var varResultExpression = Expression.Variable(resultType, "r");

			var assignExpressions = new List<Expression>();
			foreach (var map in proprtyMap)
			{
				var sourceMember = Expression.MakeMemberAccess(param, map.SourceMember);
				var destMember = Expression.MakeMemberAccess(varResultExpression, map.ResultMember);

				var sourceMemberType = GetMemberType(map.SourceMember);
				var destMemberType = GetMemberType(map.ResultMember);
				if (sourceMemberType == destMemberType)
				{
					var assign = Expression.Assign(destMember, sourceMember);
					assignExpressions.Add(assign);
				}
				else
				{
					var mapMethod = mapMethodTemplate.MakeGenericMethod(sourceMemberType, destMemberType);

					var mapCall = Expression.Call(Expression.Constant(_mapper), mapMethod, sourceMember);
					if (!sourceType.IsValueType && GetMemberType(map.SourceMember) == sourceType)
					{
						var ifExpression = Expression.IfThenElse(
							Expression.ReferenceEqual(param, sourceMember),
							Expression.Assign(destMember, varResultExpression),
							Expression.Assign(destMember, mapCall)
							);
						assignExpressions.Add(ifExpression);
					}
					else
					{
						var assign = Expression.Assign(destMember, mapCall);
						assignExpressions.Add(assign);
					}
				}
			}

			var blockExpression = Expression.Block(
				assignExpressions
				.Union(new Expression[] { Expression.Label(labelReturn, varResultExpression) })
				);
			var lambda2 = Expression.Lambda<Func<TSource, TResult, TResult>>(blockExpression, param, varResultExpression);

			sw1.Stop();

			var sw2 = new Stopwatch();
			sw2.Start();
			this.New = lambda.Compile();
			this.Map = lambda2.Compile();
			sw2.Stop();
		}

		private Type GetMemberType(MemberInfo member)
		{
			if (member is FieldInfo)
				return ((FieldInfo)member).FieldType;
			if (member is PropertyInfo)
				return ((PropertyInfo)member).PropertyType;
			return null;
		}

		private IEnumerable<MemberInfo> GetPropertiesAndFields(Type type)
		{
			BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy;

			return from m in type.GetMembers(bindingFlags)
				   where m is PropertyInfo || m is FieldInfo
				   select m;
		}

		private IEnumerable<MemberMap> CreatePropertyMap(Type sourceType, Type resultType)
		{
			IEnumerable<MemberInfo> sourceMembers = GetPropertiesAndFields(sourceType);
			IEnumerable<MemberInfo> resultMembers = GetPropertiesAndFields(resultType);

			IEnumerable<MemberMap> maps = from s in sourceMembers
											let st = GetMemberType(s)
											let st1 = st == sourceType ? resultType : st
											join r in resultMembers on new { s.Name, Type = st1 } equals
											   new { r.Name, Type = GetMemberType(r) }
											select new MemberMap
											{
												SourceMember = s,
												ResultMember = r
											};


			return maps;
		}
	}
}
